a tool for shared writing and social publishing
at feature/footnotes 159 lines 5.8 kB view raw
1import { Metadata } from "next"; 2import * as Y from "yjs"; 3import * as base64 from "base64-js"; 4 5import type { Fact } from "src/replicache"; 6import type { Attribute } from "src/replicache/attributes"; 7import { YJSFragmentToString } from "src/utils/yjsFragmentToString"; 8import { Leaflet } from "./Leaflet"; 9import { scanIndexLocal } from "src/replicache/utils"; 10import { getRSVPData } from "actions/getRSVPData"; 11import { PageSWRDataProvider } from "components/PageSWRDataProvider"; 12import { getPollData } from "actions/pollActions"; 13import { supabaseServerClient } from "supabase/serverClient"; 14import { get_leaflet_data } from "app/api/rpc/[command]/get_leaflet_data"; 15import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 16import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData"; 17import { FontLoader, extractFontsFromFacts } from "components/FontLoader"; 18 19export const preferredRegion = ["sfo1"]; 20export const dynamic = "force-dynamic"; 21export const fetchCache = "force-no-store"; 22 23type Props = { 24 // this is now a token id not leaflet! Should probs rename 25 params: Promise<{ leaflet_id: string }>; 26}; 27export default async function LeafletPage(props: Props) { 28 let { result: res } = await get_leaflet_data.handler( 29 { token_id: (await props.params).leaflet_id }, 30 { supabase: supabaseServerClient }, 31 ); 32 let rootEntity = res.data?.root_entity; 33 if (!rootEntity || !res.data || res.data.blocked_by_admin) 34 return ( 35 <NotFoundLayout> 36 <p className="font-bold">Sorry, we can't find this leaflet!</p> 37 <p> 38 This may be a glitch on our end. If the issue persists please{" "} 39 <a href="mailto:contact@leaflet.pub">send us a note</a>. 40 </p> 41 </NotFoundLayout> 42 ); 43 44 let [{ data }, rsvp_data, poll_data] = await Promise.all([ 45 supabaseServerClient.rpc("get_facts", { 46 root: rootEntity, 47 }), 48 getRSVPData(res.data.permission_token_rights.map((ptr) => ptr.entity_set)), 49 getPollData(res.data.permission_token_rights.map((ptr) => ptr.entity_set)), 50 ]); 51 let initialFacts = (data as unknown as Fact<Attribute>[]) || []; 52 53 // Extract font settings from facts for server-side font loading 54 const { headingFontId, bodyFontId } = extractFontsFromFacts(initialFacts as any, rootEntity); 55 56 return ( 57 <> 58 {/* Server-side font loading with preload and @font-face */} 59 <FontLoader headingFontId={headingFontId} bodyFontId={bodyFontId} /> 60 <PageSWRDataProvider 61 rsvp_data={rsvp_data} 62 poll_data={poll_data} 63 leaflet_id={res.data.id} 64 leaflet_data={res} 65 > 66 <Leaflet 67 initialFacts={initialFacts} 68 leaflet_id={rootEntity} 69 token={res.data} 70 initialHeadingFontId={headingFontId} 71 initialBodyFontId={bodyFontId} 72 /> 73 </PageSWRDataProvider> 74 </> 75 ); 76} 77 78export async function generateMetadata(props: Props): Promise<Metadata> { 79 let { result: res } = await get_leaflet_data.handler( 80 { token_id: (await props.params).leaflet_id }, 81 { supabase: supabaseServerClient }, 82 ); 83 let rootEntity = res.data?.root_entity; 84 if (!rootEntity || !res.data) return { title: "Leaflet not found" }; 85 let publication_data = getPublicationMetadataFromLeafletData(res.data); 86 if (publication_data) { 87 return { 88 title: publication_data.title || "Untitled", 89 description: publication_data.description, 90 }; 91 } 92 let { data } = await supabaseServerClient.rpc("get_facts", { 93 root: rootEntity, 94 }); 95 let initialFacts = (data as unknown as Fact<Attribute>[]) || []; 96 let scan = scanIndexLocal(initialFacts); 97 let firstPage = 98 scan.eav(rootEntity, "root/page")[0]?.data.value || rootEntity; 99 let pageType = scan.eav(firstPage, "page/type")[0]?.data.value || "doc"; 100 let firstBlock, secondBlock; 101 if (pageType === "canvas") { 102 [firstBlock, secondBlock] = scan 103 .eav(firstPage, "canvas/block") 104 .map((b) => { 105 let type = scan.eav(b.data.value, "block/type"); 106 if (!type[0]) return null; 107 return { 108 ...b.data, 109 type: type[0].data.value, 110 }; 111 }) 112 .filter((b) => b !== null) 113 .filter((b) => b.type === "text" || b.type === "heading") 114 .sort((a, b) => { 115 if (a.position.y === b.position.y) { 116 return a.position.x - b.position.x; 117 } 118 return a.position.y - b.position.y; 119 }); 120 } else { 121 [firstBlock, secondBlock] = scan 122 .eav(firstPage, "card/block") 123 .map((b) => { 124 let type = scan.eav(b.data.value, "block/type"); 125 return { 126 ...b.data, 127 type: type[0]?.data.value, 128 }; 129 }) 130 131 .filter((b) => b.type === "text" || b.type === "heading") 132 .sort((a, b) => (a.position > b.position ? 1 : -1)); 133 } 134 let metadata: Metadata = { title: "Untitled Leaflet", description: " " }; 135 136 let titleFact = initialFacts.find( 137 (f) => f.entity === firstBlock?.value && f.attribute === "block/text", 138 ) as Fact<"block/text"> | undefined; 139 if (titleFact) { 140 let doc = new Y.Doc(); 141 const update = base64.toByteArray(titleFact.data.value); 142 Y.applyUpdate(doc, update); 143 let nodes = doc.getXmlElement("prosemirror").toArray(); 144 metadata.title = YJSFragmentToString(nodes[0]); 145 } 146 147 let descriptionFact = initialFacts.find( 148 (f) => f.entity === secondBlock?.value && f.attribute === "block/text", 149 ) as Fact<"block/text"> | undefined; 150 if (descriptionFact) { 151 let doc = new Y.Doc(); 152 const update = base64.toByteArray(descriptionFact.data.value); 153 Y.applyUpdate(doc, update); 154 let nodes = doc.getXmlElement("prosemirror").toArray(); 155 metadata.description = YJSFragmentToString(nodes[0]); 156 } 157 158 return metadata; 159}